﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;

namespace NavmeshPathfinding
{
    public class Agent : DrawableGameComponent
    {
        Core core;
        SpriteBatch spriteBatch;

        Model model;
        Matrix modelNormalization;
        BoundingSphere modelBoundingSphere;
        Matrix[] transforms;
        static Effect cartoonEffect;
        static Effect postProcessEffect;
        static BasicEffect basicEffect;

        RenderTarget2D normalDepthRenderTarget;
        RenderTarget2D sceneRenderTarget;

        //motion
        Vector3 positionOffset;
        Vector3 position;
        Vector3 direction; //must be unit vector
        float modelGradientY;

        float speed;
        bool moving;
        Vector3 dstVertex;
        bool movingRandomly;

        bool selected;
        float floatingFrequency;

        bool displayReactionBoundary;
        bool displaySelected;

        Matrix headAnimation;
        Matrix leftArmAnimation;
        Matrix rightArmAnimation;
        float animationSpeed;

        Vector3 headPivot;
        Vector3 leftArmPivot;
        Vector3 rightArmPivot;

        public Agent(Core core) : base(core)
        {
            this.core = core;
            moving = false;
            movingRandomly = false;
            displayReactionBoundary = false;
            displaySelected = false;
            floatingFrequency = Config.Agent.FLOATING_ANIMATION_FREQ * (float)(Core.random.NextDouble() * 0.2 + 0.9);
        }

        public BoundingSphere getBoundingSphere()
        {
            if (modelBoundingSphere.Radius == 0)
            {
                Vector3 center = Vector3.Zero;

                foreach (ModelMesh mesh in model.Meshes)
                    center += Vector3.Transform(mesh.BoundingSphere.Center, transforms[mesh.ParentBone.Index]);
                center /= model.Meshes.Count;

                modelBoundingSphere.Center = center + position + positionOffset;
                modelBoundingSphere.Radius = Config.Agent.DIAMETER / 2;
            }

            return modelBoundingSphere;
        }

        public override void Initialize()
        {
            positionOffset = Vector3.Zero;
            direction = Vector3.UnitX;
            speed = 0.0f;
            selected = false;
            modelBoundingSphere = new BoundingSphere();
            modelBoundingSphere.Radius = 0;

            base.Initialize();
        }


        protected override void LoadContent()
        {
            modelBoundingSphere.Radius = 0;
            model = core.Content.Load<Model>("Eva");
            transforms = new Matrix[model.Bones.Count];
            model.CopyAbsoluteBoneTransformsTo(transforms);

            BoundingSphere sphere = getBoundingSphere();
            float modelScale = Config.Agent.DIAMETER / 2 / sphere.Radius;

            PresentationParameters pp = GraphicsDevice.PresentationParameters;

            normalDepthRenderTarget = new RenderTarget2D(GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat);
            sceneRenderTarget = new RenderTarget2D(GraphicsDevice, pp.BackBufferWidth, pp.BackBufferHeight, false, pp.BackBufferFormat, pp.DepthStencilFormat);

            spriteBatch = new SpriteBatch(GraphicsDevice);

            if (cartoonEffect == null)
                cartoonEffect = core.Content.Load<Effect>("CartoonEffect");
            if (postProcessEffect == null)
                postProcessEffect = core.Content.Load<Effect>("PostprocessEffect");

            postProcessEffect.Parameters["EdgeWidth"].SetValue(Config.Agent.EDGE_WIDTH);
            postProcessEffect.Parameters["EdgeIntensity"].SetValue(Config.Agent.EDGE_WIDTH);
            postProcessEffect.Parameters["ScreenResolution"].SetValue(new Vector2(sceneRenderTarget.Width, sceneRenderTarget.Height));
            Texture2D normalDepthTexture = normalDepthRenderTarget;
            postProcessEffect.Parameters["NormalDepthTexture"].SetValue((Texture2D)normalDepthTexture);

            postProcessEffect.CurrentTechnique = postProcessEffect.Techniques["EdgeDetect"];

            cartoonEffect.Parameters["ThresholdSensitivity"].SetValue(modelScale);
            ChangeEffectUsedByModel(model, cartoonEffect);

            //normalize model to 1.0f
            modelNormalization = Matrix.CreateScale(modelScale);

            basicEffect = new BasicEffect(GraphicsDevice);

            headPivot = model.Meshes["Mesh5"].BoundingSphere.Center;
            leftArmPivot = model.Meshes["Mesh2"].BoundingSphere.Center + new Vector3(0,0.06f,0);
            rightArmPivot = model.Meshes["Mesh1"].BoundingSphere.Center + new Vector3(0,0.12f,0);

            base.LoadContent();
        }

        public override void Update(GameTime gameTime)
        {
            modelBoundingSphere.Radius = 0;
            //update position
            if (moving)
            {
                while (true)
                {
                    if (!core.navmesh.doesPathExistFor(this))
                    {
                        if (movingRandomly)
                            core.navmesh.requestRandomDestination(this);
                        else
                        {
                            stop();
                            break;
                        }
                    }
                    else
                    {
                        dstVertex = core.navmesh.getCurrentDestination(this);

                        direction = dstVertex - position;
                        if (direction == Vector3.Zero)
                        {
                            core.navmesh.currentDestinationReached(this);
                            continue;
                        }
                        direction.Normalize();
                        break;
                    }
                }
            }

            Vector3 finalDirection;

            if (Navmesh.reactionEnabled)
            {
                Vector3 reactionVector = core.navmesh.getReaction(this, direction);
                if (reactionVector != Vector3.Zero)
                {
                    finalDirection = direction + reactionVector;
                    finalDirection.Normalize();
                }
                else
                    finalDirection = direction;
            }
            else
                finalDirection = direction;

            if (moving && speed > 0)
            {
                Vector3 newPosition;
                float movementLength = speed * (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f;
                if ((dstVertex - position).Length() < movementLength)
                {
                    newPosition = core.navmesh.checkPosition(this, position + finalDirection * movementLength);
                    core.navmesh.currentDestinationReached(this);
                }
                else
                    newPosition = core.navmesh.checkPosition(this, position + finalDirection * movementLength);
                position = newPosition;
            }

            //update speed
            if (moving && speed < Config.Agent.TERMINAL_SPEED - 0.00001f)
            {
                speed += Config.Agent.DEFAULT_ACCELERATION * (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f;
                speed = Mathf.min(speed, Config.Agent.TERMINAL_SPEED);
                animationSpeed = speed;
            }
            else if (!moving && speed > 0)
                speed = 0;

            if (!moving && animationSpeed > 0)
            {
                animationSpeed -= Config.Agent.DEFAULT_ACCELERATION * (float)gameTime.ElapsedGameTime.TotalMilliseconds / 1000.0f;
                animationSpeed = Mathf.max(animationSpeed, 0);
            }

            //update gradient
            modelGradientY = Mathf.sin(Config.Agent.ANIMATION_TILT_PERIOD * animationSpeed);

            //update floating
            positionOffset = new Vector3(0.0f, Config.Agent.FLOATING_ANIMATION_HEIGHT * modelNormalization.M11 * (Mathf.sin(floatingFrequency * (float)gameTime.TotalGameTime.TotalMilliseconds) + 1), 0.0f);

            updateAnimation();

            base.Update(gameTime);
        }

        void updateAnimation()
        {
            if (headPivot == null)
                headPivot = model.Meshes["Model5"].BoundingSphere.Center;
            if (leftArmPivot == null)
                leftArmPivot = model.Meshes["Model2"].BoundingSphere.Center;
            if (rightArmPivot == null)
                rightArmPivot = model.Meshes["Model1"].BoundingSphere.Center;

            float tilt = Config.Agent.ANIMATION_TILT_PERIOD * animationSpeed;
            headAnimation = Matrix.CreateTranslation(-headPivot) * Matrix.CreateRotationX(-tilt) * Matrix.CreateTranslation(headPivot);
            leftArmAnimation = Matrix.CreateTranslation(-leftArmPivot) * Matrix.CreateRotationZ(-tilt) * Matrix.CreateRotationX(tilt / 2) * Matrix.CreateTranslation(leftArmPivot);
            rightArmAnimation = Matrix.CreateTranslation(-rightArmPivot) * Matrix.CreateRotationZ(tilt) * Matrix.CreateRotationX(tilt / 2) * Matrix.CreateTranslation(rightArmPivot);
        }

        /// <summary>
        /// Alters a model so it will draw using a custom effect, while preserving
        /// whatever textures were set on it as part of the original effects.
        /// </summary>
        static void ChangeEffectUsedByModel(Model model, Effect replacementEffect)
        {
            // Table mapping the original effects to our replacement versions.
            Dictionary<Effect, Effect> effectMapping = new Dictionary<Effect, Effect>();

            foreach (ModelMesh mesh in model.Meshes)
            {
                // Scan over all the effects currently on the mesh.
                foreach (Effect Effect in mesh.Effects)
                {
                    if (Effect.GetType() != typeof(BasicEffect))
                        continue;

                    BasicEffect oldEffect = (BasicEffect)Effect;
                    // If we haven't already seen this effect...
                    if (!effectMapping.ContainsKey(oldEffect))
                    {
                        // Make a clone of our replacement effect. We can't just use
                        // it directly, because the same effect might need to be
                        // applied several times to different parts of the model using
                        // a different texture each time, so we need a fresh copy each
                        // time we want to set a different texture into it.
                        Effect newEffect = replacementEffect.Clone();

                        // Copy across the texture from the original effect.
                        newEffect.Parameters["Texture"].SetValue(oldEffect.Texture);
                        newEffect.Parameters["TextureEnabled"].SetValue(oldEffect.TextureEnabled);

                        effectMapping.Add(oldEffect, newEffect);
                    }
                }

                // Now that we've found all the effects in use on this mesh,
                // update it to use our new replacement versions.
                foreach (ModelMeshPart meshPart in mesh.MeshParts)
                {
                    if(effectMapping.ContainsKey(meshPart.Effect))
                        meshPart.Effect = effectMapping[meshPart.Effect];
                }
            }
        }

        public override void Draw(GameTime gameTime)
        {
            GraphicsDevice.RasterizerState = RasterizerState.CullNone;
            Vector3 realDirection = new Vector3(direction.X, -1 * modelGradientY, direction.Z);
            Matrix world = modelNormalization * Matrix.CreateWorld(position + positionOffset, realDirection, Vector3.Up);
            Matrix view = core.camera.getViewMatrix();
            Matrix projection = core.camera.getProjectionMatrix();

            basicEffect.View = view;
            basicEffect.Projection = projection;
            basicEffect.CurrentTechnique.Passes[0].Apply();

            Vector3 offset = Config.Navmesh.PATH_OFFSET * 2;

            if (displaySelected&&selected)
            {
                Circle selectionIndicator = new Circle(GraphicsDevice, position + offset, Vector3.Up, Vector3.UnitX, Config.Agent.SELECTION_RADIUS, 2 * Mathf.PI, Config.Agent.SELECTION_COLOR);
                selectionIndicator.draw();
            }

            if (displayReactionBoundary)
            {
                float width = Config.Navmesh.REACTION_BOUNDARY_WIDTH;
                float length = Config.Navmesh.REACTION_BOUNDARY_LENGTH;

                Vector3 pos = position;
                Vector3 horizontal = Vector3.Cross(direction,Vector3.Up);
                horizontal.Normalize();
                Vector3 leftBottom = pos - horizontal * width / 2 + offset ;
                Vector3 rightBottom = pos + horizontal * width / 2 + offset;
                Vector3 centerTop = pos + direction * length + offset;
                Vector3 leftTop = centerTop - horizontal * width / 2;
                Vector3 rightTop = centerTop + horizontal * width / 2;
                Rectangle boundary = new Rectangle(GraphicsDevice, leftBottom, leftTop, rightTop, rightBottom, Color.Azure);
                boundary.draw();
            }

            //storing normal and depth information
            drawModel(world, view, projection, "NormalDepth");

            /*//drawing on to a separate render target to post process
            GraphicsDevice.SetRenderTarget(sceneRenderTarget);
            GraphicsDevice.Clear(Color.CornflowerBlue);
            drawModel(world, view, projection, "Toon");

            //post process
            GraphicsDevice.SetRenderTarget(null);

            float edgeWidth = Config.Agent.EDGE_WIDTH * getBoundingSphere().Radius;
            postProcessEffect.Parameters["EdgeWidth"].SetValue(edgeWidth);
            postProcessEffect.Parameters["EdgeIntensity"].SetValue(edgeWidth);

            spriteBatch.Begin(0, BlendState.Opaque, SamplerState.PointClamp, DepthStencilState.Default, RasterizerState.CullNone, postProcessEffect);
            spriteBatch.Draw(sceneRenderTarget, Vector2.Zero, Color.White);
            spriteBatch.End();*/

            base.Draw(gameTime);
        }

        void drawModel(Matrix world, Matrix view, Matrix projection, string effectName)
        {
            BlendState oldBlendState = GraphicsDevice.BlendState;
            DepthStencilState oldStencilState = GraphicsDevice.DepthStencilState;
            GraphicsDevice.BlendState = BlendState.Opaque;
            GraphicsDevice.DepthStencilState = DepthStencilState.Default;

            foreach (ModelMesh modelMesh in model.Meshes)
            {                    
                Matrix localAnimation;
                if (modelMesh.Name == "Mesh5")
                    localAnimation = headAnimation;
                else if (modelMesh.Name == "Mesh1")
                    localAnimation = rightArmAnimation;
                else if (modelMesh.Name == "Mesh2")
                    localAnimation = leftArmAnimation;
                else
                    localAnimation = Matrix.Identity;

                foreach (Effect effect in modelMesh.Effects)
                {                    
                    /*effect.LightingEnabled = true;

                    if (core.light0.enabled)
                    {
                        effect.DirectionalLight0.Enabled = true;
                        effect.DirectionalLight0.Direction = core.light0.direction;
                        effect.DirectionalLight0.DiffuseColor = core.light0.diffuseColor;
                        effect.DirectionalLight0.SpecularColor = core.light0.specularColor;
                    }

                    if (core.light1.enabled)
                    {
                        effect.DirectionalLight1.Enabled = true;
                        effect.DirectionalLight1.Direction = core.light1.direction;
                        effect.DirectionalLight1.DiffuseColor = core.light1.diffuseColor;
                        effect.DirectionalLight1.SpecularColor = core.light1.specularColor;
                    }

                    if (core.light2.enabled)
                    {
                        effect.DirectionalLight2.Enabled = true;
                        effect.DirectionalLight2.Direction = core.light2.direction;
                        effect.DirectionalLight2.DiffuseColor = core.light2.diffuseColor;
                        effect.DirectionalLight2.SpecularColor = core.light2.specularColor;
                    }*/


                    effect.CurrentTechnique = effect.Techniques[effectName];

                    effect.Parameters["World"].SetValue(localAnimation * transforms[modelMesh.ParentBone.Index] * world);
                    effect.Parameters["View"].SetValue(view);
                    effect.Parameters["Projection"].SetValue(projection);
                    effect.Parameters["selected"].SetValue(isSelected());
                }

                modelMesh.Draw();
            }

            GraphicsDevice.BlendState = oldBlendState;
            GraphicsDevice.DepthStencilState = oldStencilState;
        }

        public void move()
        {
            moving = true;
        }

        public void stop()
        {
            moving = false;
        }

        public bool isSelected()
        {
            return selected;
        }

        public void select()
        {
            if (!selected)
            {
                selected = true;
            }
        }

        public void deselect()
        {
            if (selected)
            {
                selected = false;
            }
        }

        public Vector3 getPosition()
        {
            return position;
        }

        public void setPosition(Vector3 position)
        {
            this.position = position;
        }

        public void moveRandomly()
        {
            movingRandomly = true;
        }

        public void stopMovingRandomly()
        {
            movingRandomly = false;
        }
    }
}
